/*
 * MIT License
 *
 * Copyright (c) 2024-2048 冰羽
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package cn.star.framework.extend.core;

import cn.hutool.core.collection.CollectionUtil;
import cn.star.framework.extend.core.ForeignKey.ColumnReferenceMapping;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonProperty.Access;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.boot.model.relational.QualifiedTableName;
import org.hibernate.tool.schema.extract.spi.ColumnInformation;
import org.hibernate.tool.schema.extract.spi.ForeignKeyInformation;
import org.hibernate.tool.schema.extract.spi.IndexInformation;
import org.hibernate.tool.schema.extract.spi.PrimaryKeyInformation;
import org.hibernate.tool.schema.extract.spi.TableInformation;

/**
 * 表<br>
 *
 * @author zhaoweiping
 * @since 3.0.0
 */
@Slf4j
public class Table implements Serializable {
  /** 实例 */
  @Getter @Setter private String catalog;

  /** 数据库 */
  @Getter @Setter private String schema;

  /** 表名 */
  @Getter @Setter private String name;

  /** 注释 */
  @Getter @Setter private String comment;

  /**
   * 表类型<br>
   * 物理表、视图等
   */
  @Getter @Setter private String type;

  /** {@link Column} */
  @Getter @Setter private List<Column> columns = new ArrayList<>();

  /** {@link PrimaryKey} */
  @Getter @Setter private PrimaryKey primaryKey;

  /** {@link ForeignKey} */
  @Getter @Setter private List<ForeignKey> foreignKeys = new ArrayList<>();

  /** {@link Index} */
  @Getter @Setter private List<Index> indexes = new ArrayList<>();

  /** {@link Unique} */
  @Getter @Setter private List<Unique> uniques = new ArrayList<>();

  /** {@link GeneratorTableConfiguration} */
  @Getter
  @JsonProperty(access = Access.WRITE_ONLY)
  private final GeneratorTableConfiguration config = new GeneratorTableConfiguration();

  @Getter
  @Setter
  @JsonProperty(access = Access.WRITE_ONLY)
  private TableInformation tableInformation;

  @Getter
  @Setter
  @JsonProperty(access = Access.WRITE_ONLY)
  private List<String> columnNames;

  public Table() {}

  public Table(String name) {
    this.name = name;
  }

  public Table(String catalog, String schema, String name) {
    this.catalog = catalog;
    this.schema = schema;
    this.name = name;
  }

  public static Table to(TableInformation tableInfo, List<String> columnNames) {
    Table table = new Table();

    table.setTableInformation(tableInfo);
    table.setColumnNames(columnNames);

    List<Column> columns = new ArrayList<>();
    for (String currentTableColumnName : columnNames) {
      ColumnInformation columnInfo =
          tableInfo.getColumn(Identifier.toIdentifier(currentTableColumnName));
      if (columnInfo == null) {
        log.warn("Table: {}, column: {} is empty", tableInfo.getName(), currentTableColumnName);
      } else {
        Column column = Column.to(tableInfo, columnInfo);
        columns.add(column);
      }
    }

    QualifiedTableName named = tableInfo.getName();

    table.setCatalog(wrapper(named.getCatalogName()));
    table.setSchema(wrapper(named.getSchemaName()));
    table.setName(wrapper(named.getTableName()));

    table.setColumns(columns);
    table.setComment(tableInfo.getComment());

    applyPrimaryKeys(table, tableInfo);
    applyForeignKeys(table, tableInfo, columns);
    applyIndexes(table, tableInfo);

    return table;
  }

  private static void applyPrimaryKeys(Table table, TableInformation info) {
    try {
      List<Column> columns = table.getColumns();
      PrimaryKeyInformation primaryKey = info.getPrimaryKey();
      if (primaryKey == null) {
        log.warn("Table {} does not have a primary key", info.getName());
      } else {
        Identifier named = primaryKey.getPrimaryKeyIdentifier();
        List<Column> primaryKeyColumns =
            wrapper(primaryKey.getColumns()).stream()
                .map(
                    item ->
                        columns.stream()
                            .filter(
                                column ->
                                    column.getName().equals(wrapper(item.getColumnIdentifier())))
                            .findFirst()
                            .orElse(null))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());

        if (CollectionUtil.isNotEmpty(primaryKeyColumns)) {
          primaryKeyColumns.forEach(i -> i.setPrimary(true));
          table.setPrimaryKey(new PrimaryKey(wrapper(named), primaryKeyColumns));
        }
      }
    } catch (Exception e) {
      log.warn("obtaining [primary key] columns error, {}", e.getMessage());
    }
  }

  private static void applyForeignKeys(Table table, TableInformation info, List<Column> columns) {
    try {
      List<ForeignKeyInformation> list = wrapper(info.getForeignKeys());
      List<ForeignKey> foreignKeys = new ArrayList<>();
      for (ForeignKeyInformation foreignKeyInfo : list) {
        Identifier named = foreignKeyInfo.getForeignKeyIdentifier();
        List<ColumnReferenceMapping> mappings = new ArrayList<>();
        List<ForeignKeyInformation.ColumnReferenceMapping> ms =
            wrapper(foreignKeyInfo.getColumnReferenceMappings());
        for (ForeignKeyInformation.ColumnReferenceMapping m : ms) {
          ColumnReferenceMapping mapping = convert(m, columns);
          mappings.add(mapping);
        }
        if (CollectionUtil.isNotEmpty(mappings)) {
          ForeignKey item = new ForeignKey(wrapper(named), mappings);
          foreignKeys.add(item);
        }
      }
      table.setForeignKeys(foreignKeys);
    } catch (Exception e) {
      log.warn("obtaining [foreign key] columns error, {}", e.getMessage());
    }
  }

  private static ColumnReferenceMapping convert(
      ForeignKeyInformation.ColumnReferenceMapping m, List<Column> columns) {
    ColumnInformation source = m.getReferencingColumnMetadata();
    ColumnInformation target = m.getReferencedColumnMetadata();

    String sourceTableName =
        source.getContainingTableInformation().getName().getTableName().getText();
    String targetTableName =
        target.getContainingTableInformation().getName().getTableName().getText();
    String sourceColumnName = source.getColumnIdentifier().getText();
    String targetColumnName = target.getColumnIdentifier().getText();

    return new ColumnReferenceMapping(
        sourceTableName, targetTableName, sourceColumnName, targetColumnName);
  }

  /**
   * 是否忽略获取表结构中出现的未知异常(获取逐渐列表或索引列表)<br>
   * 主要是Oracle数据库，再获取表索引的时候会调用{@link java.sql.DatabaseMetaData#getIndexInfo(String, String, String,
   * boolean, boolean)}，在Oracle数据库驱动中实现该方法<br>
   * 驱动版本<br>
   *
   * <pre>
   *   &lt;dependency&gt;
   *     &lt;groupId&gt;com.jslsolucoes&lt;/groupId&gt;
   *     &lt;artifactId&gt;ojdbc6&lt;/artifactId&gt;
   *     &lt;version&gt;11.2.0.1.0&lt;/version&gt;
   *   &lt;/dependency&gt;
   * </pre>
   *
   * 在Oracle数据库驱动中对于方法的实现中，对Schema和TableName进行了正则判断
   *
   * <pre>.matches("([a-zA-Z]{1}\\w*(\\$|\\#)*\\w*)|(\".*)")</pre>
   *
   * <span>默认不忽略此错误</span>
   */
  private static void applyIndexes(Table table, TableInformation info) {
    try {
      List<Column> columns = table.getColumns();
      List<IndexInformation> list = wrapper(info.getIndexes());
      List<Index> indexes = new ArrayList<>();
      for (IndexInformation index : list) {
        List<Column> indexColumns =
            wrapper(index.getIndexedColumns()).stream()
                .map(
                    item ->
                        columns.stream()
                            .filter(
                                column ->
                                    column.getName().equals(wrapper(item.getColumnIdentifier())))
                            .findFirst()
                            .orElse(null))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());

        if (CollectionUtil.isNotEmpty(indexColumns)) {
          indexes.add(new Index(wrapper(index.getIndexIdentifier()), indexColumns));
        }
      }
      table.setIndexes(indexes);
    } catch (Exception e) {
      log.warn("obtaining [index] columns error, {}", e.getMessage());
    }
  }

  private static String wrapper(Identifier identifier) {
    return identifier == null ? null : identifier.getText();
  }

  private static <T> List<T> wrapper(Iterable<T> iterable) {
    return iterable == null ? new ArrayList<>() : CollectionUtil.newArrayList(iterable);
  }

  /** 生成Sql配置 */
  public static class GeneratorTableConfiguration {
    /** {@link org.hibernate.mapping.Table#isAbstract()} */
    @Getter @Setter private boolean isAbstract = false;

    /**
     * {@link org.hibernate.mapping.Table#isQuoted()} 、{@link
     * org.hibernate.mapping.Column#isQuoted()}<br>
     * 主要解决关键字的问题，默认false，即不需要包装
     */
    @Getter @Setter private boolean quoted = false;

    /** 支持删除旧的列 */
    @Getter @Setter private boolean dropColumnEnable = false;

    /** 支持修改列 */
    @Getter @Setter private boolean alterColumnEnable = false;
  }
}
